/* eslint-disable guard-for-in */
import { List } from 'immutable';
import memorize from 'lodash-decorators/memoize';
import mixin from '../utils/mixin';
import Document from '../model/document';
import Block from '../model/block';
import generateKey from '../utils/generate-key';

// function assertKey(arg) {
//   if (typeof arg === 'string') return arg;
//   throw new Error(
//     `Invalid \`key\` argument! It must be a key string, but you passed: ${arg}`
//   );
// }

function assertPath(path) {
  if (Array.isArray(path)) {
    return List(path);
  }
  if (List.isList(path)) {
    return path;
  }
  throw new Error(
    `Invalid \`path \` argument! It must be array, but got: ${path}`
  );
}

class ElementInterface {
  insertNodeByPath(path, node) {
    path = this.resolvePath(path);
    const index = path.last();
    const parentPath = path.slice(0, -1);
    let parent = this.getNode(parentPath);
    const nodes = parent.nodes.splice(index, 0, node);
    parent = parent.set('nodes', nodes);
    const ret = this.replaceNode(parentPath, parent);
    return ret;
  }

  removeNode(path) {
    path = this.resolvePath(path);
    const keyPath = path.flatMap(x => [ 'nodes', x ]);
    return this.deleteIn(keyPath);
  }

  moveNode(source, target) {
    const srcPath = this.resolvePath(source);
    const node = this.getDescendant(srcPath);
    let newPath = this.resolvePath(target);
    if (srcPath.size < newPath.size) {
      for (let i = 0; i < srcPath.size; i++) {
        if (srcPath.get(i) < newPath.get(i)) {
          // if target path is above source path, should ajust new path
          newPath = newPath.update(i, index => index - 1);
        }
      }
    }
    let ret = this.removeNode(srcPath);
    ret = ret.insertNodeByPath(newPath, node);
    return ret;
  }

  replaceNode(path, node) {
    path = this.resolvePath(path);
    if (!path) {
      throw new Error(
        `Unable to replace a node because it could not be found in the first place: ${path}`
      );
    }
    if (!path.size) return node;
    const keyPath = path.flatMap(x => [ 'nodes', x ]);
    const ret = this.setIn(keyPath, node);
    return ret;
  }

  resolvePath(path, index) {
    if (typeof path === 'string') {
      path = this.getPath(path);
      if (index !== null && index !== undefined) {
        path = path.concat(index);
      }
    }
    return assertPath(path);
  }

  contains(key) {
    const path = this.getPath(key);
    if (path && path.size > 0) {
      return true;
    }
    return false;
  }

  getPath(key) {
    if (Array.isArray(key)) {
      return List(key);
    }
    if (List.isList(key)) {
      return key;
    }
    const dict = this.getKeysToPathsTable();
    const path = dict[key];
    return List(path);
  }

  @memorize
  getKeysToPathsTable() {
    const dic = {
      [this.key]: [],
    };

    if (this.nodes) {
      this.nodes.forEach((node, i) => {
        const nested = node.getKeysToPathsTable();

        for (const key in nested) {
          const path = nested[key];
          dic[key] = [ i, ...path ];
        }
      });
    }
    return dic;
  }

  getNode(path) {
    path = this.resolvePath(path);
    if (!path) return null;
    const node = path.size ? this.getDescendant(path) : this;
    return node;
  }

  getDescendant(path) {
    path = this.resolvePath(path);
    if (!path) return null;

    const deep = path.flatMap(x => [ 'nodes', x ]);
    const node = this.getIn(deep);
    return node;
  }

  getParent(path) {
    path = this.resolvePath(path);
    if (!path) {
      return null;
    }
    if (!path.size) {
      return null;
    }
    const parentPath = path.slice(0, -1);
    return this.getNode(parentPath);
  }

  getNextPath(path) {
    path = this.resolvePath(path);
    if (!path) {
      return null;
    }
    if (!path.size) {
      return null;
    }
    return path.update(-1, index => index + 1);
  }

  getLastPath(path) {
    path = this.resolvePath(path);
    if (!path) {
      return null;
    }
    if (!path.size) {
      return null;
    }
    return path.update(-1, index => index - 1);
  }

  clone() {
    return this.set('key', generateKey());
  }
}

mixin(ElementInterface, [ Document, Block ]);
